package astoliarskyi.lab5.conpool;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class ConnectionPool {

	private final static int CONNECTIONS_AMOUNT = 30;
	private final static int WAITING_TIMEOUT = 1000;
	private final static int IDLE_TIMEOUT = 500;

	private final String url;
	private final String user;
	private final String passwd;
	private boolean initialized = false;

	private final Queue<Connection> availableConn
			= new ConcurrentLinkedQueue<Connection>();
	private final Queue<FixingIdleTimeConnection> checkoutedConn
			= new ConcurrentLinkedQueue<>();

	private final Semaphore available = new Semaphore(CONNECTIONS_AMOUNT, true);

	public ConnectionPool(String url, String user, String passwd) {
		this.url = url;
		this.user = user;
		this.passwd = passwd;
	}

	public void initialize() {
		for (int i = 0; i < CONNECTIONS_AMOUNT; i++) {
			try {
				Connection conn = DriverManager.getConnection(url, user, passwd);
				availableConn.add(conn);
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
		Thread connShepherd = new Thread(new ConnectionShepherd());
		connShepherd.setDaemon(true);
		connShepherd.start();
		initialized = true;
	}
	
	public Connection checkout() throws TimeoutException {
		if (!initialized) initialize();
		boolean connAcquired = false;
		Connection conn = null;
		try {
			connAcquired = available.tryAcquire(WAITING_TIMEOUT, TimeUnit.MILLISECONDS);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		if (connAcquired) {
			conn = availableConn.poll();
		} else {
			throw new TimeoutException();
		}
		FixingIdleTimeConnection connFixIdleTime
				= new FixingIdleTimeConnection(conn, this, IDLE_TIMEOUT);
		checkoutedConn.add(connFixIdleTime);
		return connFixIdleTime;
	}

	public void checkin(Connection c) {
		if (checkoutedConn.contains(c)) {
			if (c instanceof FixingIdleTimeConnection) {
				checkoutedConn.remove(c);
				c = ((FixingIdleTimeConnection) c).getWrappedConnection();
			}
			availableConn.add(c);
			available.release();
		}
	}

	private class ConnectionShepherd implements Runnable {

		@Override
		public void run() {
			for (FixingIdleTimeConnection fitConn : checkoutedConn) {
				Connection conn = fitConn.checkConnectionExpiration();
				if (conn != null) {
					checkoutedConn.remove(fitConn);
					availableConn.add(conn);
				}
			}

			try {
				Thread.sleep(100);
			} catch (InterruptedException e) { /*Won't be interrupted. */ }

		}

	}
}
